バグトラッキングシステム(以降、BTS)のチケットデータには様々な情報が記録されており品質分析に使わない手はありません。バグチケットの分析は個々のチケットに対する定性分析を行うことが多いですが、ODC分析のようにクロス集計を用いる方法もあります。ODC分析にはODC分析用のタグが必要ですが、ここでは基本的なバグチケットにある情報(項目)を用いた可視化の方法を探って行きます。
 

なお、本ページではR version 3.4.4 (2018-03-15)の標準パッケージ以外に以下の追加パッケージを用いています。
 

Package Version Description
tidyverse 1.2.1 Easily Install and Load the ‘Tidyverse’

 
また、本ページでは以下のデータセットを用いています。
 

Dataset Package Version Description
redmine N/A N/A Redmine Issues

 

バグチケットはRedmine が公開しているRedmine自体のバグチケットを用います。RedmineはGPL v2ライセンスの下で提供されているオープンソースのプロジェクト管理ソフトウェアです。上の表の場所でチケットを公開していますが、一度に50レコードまでしかダウンロードできないため事前にこちらで取得したレコードをデータフレーム形式にまとめたものを利用します。なお、Redmine ではREST APIを利用してJSON形式でのチケットう情報の取得が可能ですが、REST APIでは一度に25件しかチケットを取得できない点に注意してください。
 

チケット情報のインポート

前述のように今回は事前に整理したデータフレーム形式のチケット情報を用いますが、実際にはBTSのAPI機能やBTSのDBMSから直接取得することをおすゝめします。直接取得できない場合は、CSVファイルへエクスポートするなどの方法を取ってください。
 

チケットの項目

今回用いるRedmineのバグチケットの項目を簡単に説明してます。基本的な項目のみが用意されています。実際は因子型になっている項目をここでは文字型として扱っている点に注意してください。
   

項目 概要 データ型
# 識別番号(Primary Key) 整数型
プロジェクト 属するプロジェクト 文字型(因子型)
トラッカー 大分類 文字型(因子型)
親チケット 親子関係を定義したい場合に用いる 文字型
ステータス 対応状況 文字型(因子型)
優先度 対応優先度 文字型(因子型)
題名 タイトル 文字型
作成者 作成者 文字型(因子型)
担当者 対応担当者 文字型(因子型)
更新日 更新日時 日時型(POSIXct)
カテゴリ 分類(任意に利用設定できる) 文字型(因子型)
対象バージョン チケット対処したバージョン 文字型
開始日 対応を開始した日 日付型
期日 対応予定期間 日付型
予定工数 対応予定工数 数値型
進捗率 対応の進捗率 数値型(%表記)
作成日 作成日時 日時型(POSIXct)
終了日 対応完了日時 日時型(POSIXct)
関連するチケット 関係するチケット番号 文字型
Resolution 解決結果(非標準) 文字型(因子型)
Affected version 影響のあるバージョン 文字型
説明 詳細 文字型

 

チケットデータ

実際のデータは以下のような四千レコード弱のデータです。
 

(redmine <- "./data/redmine.csv" %>% 
  readr::read_csv(local = locale(encoding = "UTF-8")))

 

分析のための前処理

分析に必要な前処理を行っておきます。作成日と終了日のデータは実際は日時データになっていますので日データに変換して、必要な項目のみを抽出しておきます。
 

(x <- redmine %>% 
  dplyr::select(no = `#`, tracker = `トラッカー`, status = `ステータス`,
                priority = `優先度`, category = `カテゴリ`,
                version = `対象バージョン`, affected = `Affected version`, 
                open = `作成日`, close = `終了日`, subject = `題名`,
                assignee = `担当者`) %>% 
  dplyr::mutate(open = lubridate::date(open), close = lubridate::date(close)))

 

チケットの集計

データフレームに対する集計を行うにはdplyr::group_by関数+dplyr::summarise関数またはdplyr::count関数を用いるのが便利です。
 
なお、以下の二つのコードは共にdataに含まれるkey変数の水準毎に個数を数えるもので結果は等価になります。
 

dplyr::group_by(data, key) %>% 
  dplyr::summarise(n = n())
dplyr::count(data, kye)

 

単純集計

以下のようにsummary関数では表示されない文字型(因子型)のデータの傾向を見るために単純集計を行ってみます。

summary(x)
       no          tracker             status            priority        
 Min.   :13710   Length:3826        Length:3826        Length:3826       
 1st Qu.:16723   Class :character   Class :character   Class :character  
 Median :20347   Mode  :character   Mode  :character   Mode  :character  
 Mean   :20647                                                           
 3rd Qu.:24342                                                           
 Max.   :28967                                                           
                                                                         
   category           version            affected        
 Length:3826        Length:3826        Length:3826       
 Class :character   Class :character   Class :character  
 Mode  :character   Mode  :character   Mode  :character  
                                                         
                                                         
                                                         
                                                         
      open                close              subject         
 Min.   :2013-04-08   Min.   :2010-07-19   Length:3826       
 1st Qu.:2014-04-19   1st Qu.:2014-07-21   Class :character  
 Median :2015-07-16   Median :2015-08-31   Mode  :character  
 Mean   :2015-08-17   Mean   :2015-09-10                     
 3rd Qu.:2016-11-13   3rd Qu.:2016-11-18                     
 Max.   :2018-06-06   Max.   :2018-06-06                     
                      NA's   :870                            
   assignee        
 Length:3826       
 Class :character  
 Mode  :character  
                   
                   
                   
                   

 

Tracker

x %>% 
  dplyr::count(tracker)

 

Status

x %>% 
  dplyr::count(status)

 

Priority

x %>% dplyr::count(priority)

 

Category

x %>% 
  dplyr::count(category)

 

Version

x %>% 
  dplyr::count(version)

 
### Affected

x %>% 
  dplyr::count(affected)

 

Open

x %>% 
  dplyr::count(open)

クロス集計

単純集計では見えにくい傾向はクロス集計を行いことで見えてくることもあります。クロス集計はdplyr::count関数またはdplyr::group_by関数に複数の変数を指定し、tidyr::spread関数で変形することで簡単にクロス集計表が作成できます。

dplyr::count(x, key1, key2) %>% 
  tidyr::spread(key1, value = n)

 

Tracker and Status

x %>% 
  dplyr::count(tracker, status) %>% 
  tidyr::spread(key = tracker, value = n)

 

Tracker and Priority

大半のチケットがステータスがClosedな対応が完了しているチケットですので、Closedを除くチケットに対する優先度を見て見ます。
 

x %>% 
  dplyr::filter(status != "Closed") %>% 
  dplyr::count(tracker, priority) %>% 
  tidyr::spread(key = tracker, value = n)

 

Priority and Status

Defectチケット

x %>% 
  dplyr::filter(status != "Closed" & tracker == "Defect") %>% 
  dplyr::count(priority, status) %>% 
  tidyr::spread(key = priority, value = n)

 

Patchチケット

x %>% 
  dplyr::filter(status != "Closed" & tracker == "Patch") %>% 
  dplyr::count(priority, status) %>% 
  tidyr::spread(key = priority, value = n)

対象レコードの抽出

クロス集計の結果優先度がUrgentであるチケットがあることが分かりましたので、対象が何かを表示させてみます。クロス集計で検索条件が分かっていますので絞り込むだけです。
 

x %>% 
  dplyr::filter(status != "Closed" & tracker == "Defect") %>% 
  dplyr::filter(priority == "Urgent") %>% 
  dplyr::select(no, tracker, status, subject, assignee)

 

Priority and Category

x %>% 
  dplyr::filter(status != "Closed" & tracker == "Defect") %>% 
  dplyr::count(category, priority) %>% 
  tidyr::spread(key = priority, value = n)

 

Priority and Assignee

x %>% 
  dplyr::filter(status != "Closed" & tracker == "Defect") %>% 
  dplyr::count(priority, assignee) %>% 
  tidyr::spread(key = priority, value = n)

 
このデータではUrgentなチケットに担当者が割り当てられていないことが分かります。
 

期間集計

ある一定期間ごとに集計する場合は日時データを日、週、月、四半期、年などに変換し変換後のデータをdplyr::count関数で集計することで期間の変化を確認できるようになります。
 

日次集計

日次で集計する場合は日時の場合はlubridate::date関数で日付に変換しておきます。データがない日は集計対象外となります。稼働が発生していてデータがないのか、稼働が発生していないからデータがないのかで意味が変わってきますので集計の際には注意してください。
 

起票されたチケットの推移

x %>% 
  dplyr::filter(open >= "2018-1-1") %>% 
  dplyr::mutate(flag = ifelse(is.na(open), 0, 1)) %>% 
  dplyr::group_by(open) %>% 
  dplyr::summarise(ticket = sum(flag)) %>% 
  dplyr::arrange(open) %>% 
  dplyr::mutate(`累計` = cumsum(ticket),
                `前日との差` = ticket - dplyr::lag(ticket))

 

週次集計

“週”を求めるにはlubridate::week関数を用います。ただし、lubridate::week関数は1から53までの値しか返しませんので、年をまたぐ際はlubridate::year関数などを用いて年の識別ができるようにしてください。
 

起票されたチケットの推移

x %>% 
  dplyr::filter(open >= "2018-1-1") %>% 
  dplyr::mutate(week = lubridate::week(open)) %>% 
  dplyr::mutate(flag = ifelse(is.na(open), 0, 1)) %>% 
  dplyr::group_by(week) %>% 
  dplyr::summarise(ticket = sum(flag)) %>% 
  dplyr::arrange(week) %>% 
  dplyr::mutate(`累計` = cumsum(ticket),
                `前週との差` = ticket - dplyr::lag(ticket))

完了したチケットの推移

x %>% 
  dplyr::filter(close >= "2018-1-1") %>% 
  dplyr::mutate(week = lubridate::week(close)) %>% 
  dplyr::mutate(flag = ifelse(is.na(close), 0, 1)) %>% 
  dplyr::group_by(week) %>% 
  dplyr::summarise(ticket = sum(flag)) %>% 
  dplyr::arrange(week) %>% 
  dplyr::mutate(`累計` = cumsum(ticket),
                `前週との差` = ticket - dplyr::lag(ticket))

 

月次集計

“月”を求めるにはlubridate::month関数を用います。ただし、lubridate::month関数は1から12までの値しか返しませんので、年をまたぐ際はlubridate::year関数などを用いて年の識別ができるようにしてください。
 

起票されたチケットの推移

x %>% 
  dplyr::filter(open >= "2018-1-1") %>% 
  dplyr::mutate(month = lubridate::month(open)) %>% 
  dplyr::mutate(flag = ifelse(is.na(open), 0, 1)) %>% 
  dplyr::group_by(month) %>% 
  dplyr::summarise(ticket = sum(flag)) %>% 
  dplyr::arrange(month) %>% 
  dplyr::mutate(`累計` = cumsum(ticket),
                `前月との差` = ticket - dplyr::lag(ticket))

 

四半期次集計

“四半期”を求めるにはlubridate::quarter関数を用います。年をまたぐ際はwith_yearオプションを使用すると年の識別ができるようになります。また、第一四半期が1月以外から始まる場合はfiscal_startオプションを使用してください。
 

起票されたチケットの推移

x %>% 
  dplyr::mutate(quarter = lubridate::quarter(open, with_year = TRUE,
                                             fiscal_start = 1)) %>% 
  dplyr::mutate(flag = ifelse(is.na(open), 0, 1)) %>% 
  dplyr::group_by(quarter) %>% 
  dplyr::summarise(ticket = sum(flag)) %>% 
  dplyr::arrange(quarter) %>% 
  dplyr::mutate(`累計` = cumsum(ticket),
                `前四半期との差` = ticket - dplyr::lag(ticket))

 

年次集計

“年”を求めるにはlubridate::year関数を用います。
 

起票されたチケットの推移

x %>% 
  dplyr::mutate(year = lubridate::year(open)) %>% 
  dplyr::mutate(flag = ifelse(is.na(open), 0, 1)) %>% 
  dplyr::group_by(year) %>% 
  dplyr::summarise(ticket = sum(flag)) %>% 
  dplyr::arrange(year) %>% 
  dplyr::mutate(`累計` = cumsum(ticket),
                `前年との差` = ticket - dplyr::lag(ticket))

チケットの可視化

分布の可視化

分布を可視化する代表的な方法としてはヒストグラム、箱ひげ図などがあります。
 

集計の可視化

集計結果を可視化する方法としては棒グラフ、円グラフ、それらの層別グラフなどがあります。
 

推移の可視化

推移を可視化する代表的な方法としては折れ線グラフ、棒グラフがあります。
 

週次傾向の可視化

前出の週次集計を可視化してみます。加えて平均起票数を基準として前週との起票数の差を折れ線グラフで表示しています。
 

起票されたチケットの推移

open <- x %>% 
  dplyr::filter(open >= "2018-1-1") %>% 
  dplyr::mutate(week = lubridate::week(open)) %>% 
  dplyr::mutate(flag = ifelse(is.na(open), 0, 1)) %>% 
  dplyr::group_by(week) %>% 
  dplyr::summarise(open = sum(flag)) %>% 
  dplyr::arrange(week) %>% 
  dplyr::mutate(cumopen = cumsum(open), diff = open - dplyr::lag(open))

open %>% 
  dplyr::mutate(diff_offset = diff + round(mean(open, na.rm = TRUE))) %>% 
  ggplot2::ggplot(ggplot2::aes(x = week)) +
    ggplot2::geom_bar(ggplot2::aes(y = open), stat = "identity", alpha = 0.25) + 
    ggplot2::geom_hline(yintercept = round(mean(open$open, na.rm = TRUE)),
                        colour = "#00bfc4", linetype = "dashed") + 
    ggplot2::geom_line(ggplot2::aes(y = diff_offset), colour = "#00bfc4",
                       size = 0.75)

 

完了したチケットの推移

close <- x %>% 
  dplyr::filter(close >= "2018-1-1") %>% 
  dplyr::mutate(week = lubridate::week(close)) %>% 
  dplyr::mutate(flag = ifelse(is.na(close), 0, 1)) %>% 
  dplyr::group_by(week) %>% 
  dplyr::summarise(close = sum(flag)) %>% 
  dplyr::arrange(week) %>% 
  dplyr::mutate(cumclose = cumsum(close), diff = close -dplyr::lag(close))

close %>% 
  dplyr::mutate(diff_offset = diff + round(mean(close, na.rm = TRUE))) %>% 
  ggplot2::ggplot(ggplot2::aes(x = week)) +
    ggplot2::geom_bar(ggplot2::aes(y = close), stat = "identity", alpha = 0.25) + 
    ggplot2::geom_hline(yintercept = round(mean(close$close, na.rm = TRUE)),
                        colour = "#f8766d", linetype = "dashed") + 
    ggplot2::geom_line(ggplot2::aes(y = diff_offset), colour = "#f8766d",
                       size = 0.75)

 

オープン・クローズチャート

前出の週次の集計から累計データに着目したものがオープン・クローズチャートです。オープン・クローズチャートはチケットの対応状況が一目で分かるグラフです。  

open_ticket <- open %>% 
  dplyr::full_join(close, by = "week") %>% 
  dplyr::select(week, open, close) %>%
  tidyr::gather(key, value, -week)

open %>% 
  dplyr::full_join(close, by = "week") %>% 
  dplyr::select(week, cumopen, cumclose) %>%
  tidyr::gather(key, value, -week) %>% 
  dplyr::left_join(open_ticket, by = "week") %>% 
  ggplot2::ggplot(ggplot2::aes(x = week)) + 
    ggplot2::geom_bar(ggplot2::aes(y = value.y, fill = key.y),
                      stat = "identity", alpha = 0.5, position = "dodge") +
    ggplot2::geom_line(ggplot2::aes(y = value.x, colour = key.x),
                       stat = "identity", size = 0.75) + 
    ggplot2::scale_color_hue(name = "累計",
                             labels = c(cumclose = "Closed", cumopen = "Open")) +
    ggplot2::scale_fill_hue(name = "週次",
                            labels = c(close = "Closed", open = "Open"))

 

滞留期間の可視化

優先度ごとにチケットがどれだけ滞留されているか可視化してみます。
 

x %>% 
  dplyr::filter(open >= "2018-1-1") %>% 
  dplyr::filter(status != "Closed") %>% 
  dplyr::mutate(days = lubridate::today() - open + 1) %>% 
  dplyr::group_by(priority) %>% 
  dplyr::summarise(min = min(days), med = median(days), max = max(days))
x %>% 
  dplyr::filter(open >= "2018-1-1") %>% 
  dplyr::filter(status != "Closed") %>% 
  dplyr::mutate(days = lubridate::today() - open + 1) %>% 
  ggplot2::ggplot(ggplot2::aes(x = priority, y = days)) + 
    ggplot2::geom_boxplot()

 
同様にカテゴリごとの滞留期間を可視化してみます。

x %>% 
  dplyr::filter(status != "Closed") %>% 
  dplyr::mutate(days = lubridate::today() - open + 1) %>% 
  dplyr::group_by(category) %>% 
  dplyr::summarise(min = min(days), med = median(days), max = max(days),
                   mode = which.max(table(days)))
x %>% 
  dplyr::filter(open >= "2018-1-1") %>% 
  dplyr::filter(status != "Closed") %>% 
  dplyr::mutate(days = lubridate::today() - open + 1) %>% 
  ggplot2::ggplot(ggplot2::aes(x = category, y = days)) + 
    ggplot2::geom_boxplot()

滞留期間のヒストグラム  

対処期間の可視化

カテゴリごとのチケット対処期間(開始日から終了日までの期間)を可視化してみます。
 

x %>% 
  dplyr::filter(status == "Closed") %>% 
  dplyr::mutate(days = close - open + 1) %>% 
  dplyr::group_by(category) %>% 
  dplyr::summarise(min = min(days), med = median(days), max = max(days),
                   mode = which.max(table(days)))
x %>% 
  dplyr::filter(status == "Closed") %>% 
  dplyr::mutate(days = close - open + 1) %>% 
  ggplot2::ggplot(ggplot2::aes(x = category, y = days)) + 
    ggplot2::geom_boxplot()

 

対処推移の可視化

チケットのオープン数とクローズ数の推移を可視化してみます。データ数が多いので2018年4月1日以降のチケットのみを対象としています。
 

df_date <- seq(from = range(x$open)[1], to = range(x$open)[2], by = 1) %>%
  as.data.frame()
names(df_date) <- c("Date")
df_date <- df_date %>% 
  dplyr::filter(Date >= "2018-4-1")

open <- x %>% 
  dplyr::select(ID = no, Tracker = tracker, Status = status,
                OpenDateTime = open, CloseDateTime = close) %>% 
  dplyr::mutate_at(vars(Status),
                   funs(replace(., (. != "Closed"), "Open"))) %>% 
  dplyr::filter(Tracker == "Defect" & Status != "Closed") %>% 
  dplyr::mutate(Date = dplyr::if_else(Status == "Closed", 
                                      lubridate::as_date(CloseDateTime),
                                      lubridate::as_date(OpenDateTime))) %>% 
  dplyr::arrange(Date) %>% 
  dplyr::count(Date, Status) %>% 
  dplyr::right_join(df_date, by = "Date") %>% 
  dplyr::mutate_at(vars(n), funs(replace(., is.na(.), 0))) %>% 
  dplyr::mutate_at(vars(Status), funs(replace(., is.na(.), "Open"))) %>% 
  dplyr::mutate(Cumsum = cumsum(n))

closed <- x %>% 
  dplyr::select(ID = no, Tracker = tracker, Status = status,
                OpenDateTime = open, CloseDateTime = close) %>% 
  dplyr::mutate_at(vars(Status),
                   funs(replace(., (. != "Closed"), "Open"))) %>% 
  dplyr::filter(Tracker == "Defect" & Status == "Closed") %>% 
  dplyr::mutate(Date = dplyr::if_else(Status == "Closed", 
                                      lubridate::as_date(CloseDateTime),
                                      lubridate::as_date(OpenDateTime))) %>% 
  dplyr::arrange(Date) %>% 
  dplyr::count(Date, Status) %>% 
  dplyr::right_join(df_date, by = "Date") %>% 
  dplyr::mutate_at(vars(n), funs(replace(., is.na(.), 0))) %>% 
  dplyr::mutate_at(vars(Status), funs(replace(., is.na(.), "Closed"))) %>% 
  dplyr::mutate(Cumsum = cumsum(n))

open %>% 
  dplyr::bind_rows(closed) %>% 
  dplyr::arrange(Date) %>% 
  ggplot2::ggplot(ggplot2::aes(x = Date, y = Cumsum)) +
    # ggplot2::geom_area(aes(fill = Status), alpha = 0.5, position = "stack")
    # ggplot2::geom_area(aes(fill = Status), alpha = 0.5, position = "identity") +
    ggplot2::geom_line(ggplot2::aes(colour = Status)) +
    # ggplot2::geom_smooth(ggplot2::aes(colour = Status)) +
    ggplot2::geom_bar(ggplot2::aes(y = n, fill = Status), stat = "identity") + 
    ggplot2::labs(x = "days", y = "Number of tickets",
                  title = "Open - Closed Chart") + 
    NULL

Tips

集計を加える

row_sum <- x %>% 
  dplyr::count(tracker, status) %>% 
  tidyr::spread(key = tracker, value = n) %>% 
  dplyr::mutate(Sum = ifelse(is.na(Defect), 0, Defect) + 
                  ifelse(is.na(Patch), 0, Patch)) %>% 
  dplyr::summarise_if(is.numeric, sum, na.rm = TRUE) %>% 
  dplyr::mutate(status = NA) %>% 
  dplyr::select(status, Defect, Patch, Sum)

x %>% 
  dplyr::count(tracker, status) %>% 
  tidyr::spread(key = tracker, value = n) %>% 
  dplyr::mutate(Sum = ifelse(is.na(Defect), 0, Defect) + 
                  ifelse(is.na(Patch), 0, Patch)) %>% 
  dplyr::bind_rows(row_sum)